// TODO: 具名函数
// function 方法名(){}
// function 函数名(参数列表){ 函数体 }

// TODO：匿名函数
// 使用方式：函数表达式、立即执行函数
// 函数表达式：将匿名函数赋值给“变量”，在通过变量名进行调用
let fn = function () {
    console.log("函数表达式");
}
// 调用
fn();

// TODO：具名函数与函数表达式（匿名函数）的区别
// a.具名函数的调用可以写在任意位置，函数表达式的调用，必须写在声明之后
// 如：
// 具名函数：
test01();       // 可以在声明定义之前调用
function test01() { }
test01();       // 也可以在声明定义之后调用
// 函数表达式的匿名函数
let test02 = function () { }
test02();       // 只能在变量赋值之后进行调用
// 原因：在JS中，变量必须是先声明在使用，函数表达式也是变量声明赋值的一种形式
// 也必须要遵循变量的使用的语法


// TODO：立即执行函数
(function () { console.log("立即执行函数"); })()



// TODO:实参设置默认值
// 函数再调用时,没有传入形参,则实参的默认值就是undefined
// 此时,我们可以给实参设置一个默认值
// 如下: 实参设置默认值
function sum(x = 0, y = 10) { }


// TODO：函数返回值
// 函数的返回值类型：任意，如：函数、数组、基本数据类型等等
// 当函数需要返回数据结果时,我们可以使用return返回数据
// 如:
function sum(x, y) {
    return x + y;
}
// 注意:
// return:结束函数,所以return后面后面不要再写代码,后面的代码不会被执行
// 如:
function sum(x, y, z) {
    return x + y + z;               // return:返回数据,并结束函数
    console.log(x + y + z);         // 不会被执行
}
// 函数可以没有return,此时函数默认返回值为undefined


// TODO：函数覆盖问题
// 如果写了两个一样的函数,则后面的函数会覆盖前面的函数
// 如:
function test() { console.log(1); }
function test() { console.log(2); }     // 覆盖了前面的test
test();     // 2


// TODO: 函数内变量的特殊用法
// 在函数内部，变量不声明，直接赋值，被当作全局变量处理
function test() {
    num = 10;           // 等价于 window.num = 10;
}
test();     // 调用函数，num会被当作全局变量创建
console.log(num);



// TODO：函数的参数
// 注意：
//      a. 函数在调用时，不会检测传入的参数的“个数”
// 如：
function sum02(a, b) { }        // 定义了两个形参
// 实参数量少于形参的数量，则没有对应的实参的形参将是undefined
sum02(1);
// 多余的实参不会被赋值
sum02(1, 2, 3);

// TODO：函数的剩余参数
// 语法：...变量名
// 它是一个数组，用来接收所有剩余的实参
// 如：
function sum06(a, b, ...arr) { }
// a = 1, b = 2
// 其他的实参，全部存入...arr数组中
sum06(1, 2, 3, 4, 5);
// 注意：...arr只能写在参数列表的最后面


// TODO：实参的类型
// 在JS中调用方法传入的参数，可以是“任意类型”，即：基本数据类型、对象（函数、数组等等）



// TODO：break、continue、return
// break：退出当前循环
// continue：跳过当次循环
// return：结束当前函数执行


// TODO：作用域
// 全局作用域：
//          a. 直接编写在script标签中的JS代码，都在全局作用
//          b. 全局作用域在页面打开时创建，在页面关闭时销毁
//          c. 全局作用域有一个全局对象window（由浏览器创建），我们可以直接使用
//注意：在全局作用域中创建的变量都会作为window对象的属性进行保存
//      在全局作用域中创建的函数都会作为window对象的方法进行保存
// 函数作用域：
//          调用函数时创建函数作用域，函数执行完毕后，函数作用域销毁
//          每调用一次函数就会创建一个新的函数作用域，它们之间相互独立
//          在函数作用域创建的变量被称为“局部变量”
// 注意：
//          当在函数作用域内操作一个变量时，它会先在自身作用域中寻找
//          如果有就直接使用，如果没有则向上一级作用域中寻找，直到找到
//          全局作用域，如果在全局作用域中依然没有找到，则会报错


// TODO：var变量提升
// 在JS中使用var声明变量，会出现变量提升的问题
// 变量提升：在运行JS代码前，先扫描JS文件，找到文件中所有使用
// var声明的变量，随后将var声明的变量的声明提升到其作用域的最上面
// 注意：只是将 “变量声明” 提升了，赋值还是在原来的位置
// 如：
console.log(m);
var m = 10;
function test04() {
    console.log(n);
    var n = 20;
}
// 变量提升后代码（由浏览器进行操作）
var m;
console.log(m);             // 结果：undefined，还没有进行赋值
m = 10;
function test04() {
    var n;
    console.log(n);
    n = 20;
}
// 注意：let、const不存在变量提升的
// let、const声明的变量会产生块级作用域
// var声明的变量不会产生块级作用域



// TODO：函数声明提前
// 使用具名函数创建函数时，会存在“函数声明提前”的操作
// 函数声明提前：使用具名函数创建函数：function 函数名(){}
// 它会在所有代码执行之前被创建，类似于var，将函数放到了最上面先创建
// 如：
test03();
function test03() { }
// 触发函数声明提前
function test03() { }
test03();
// 所以使用具名函数创建函数，可以将函数的调用放在创建函数的前面，就是
// 因为有“函数声明提前”

// 而使用匿名函数就是不行将函数的调用，放在函数创建之前的
// 如：
test04();
var test04 = function () { }
// 使用变量的形式创建，只会触发var声明提升
// 即：
var test04;     // 此时变量并没有赋值，默认值：undefined
test04();       // undefined调用函数，会抛出异常
test04 = function () { };

// let、const声明函数更加不能将函数调用放在函数创建之前了
// let、const没有变量提升，当函数还没有定义时，就调用函数是不被允许的
test05();       // test05还没有声明定义，就开始使用了
let test05 = function () { }  // 没有变量提升，也没有函数声明提前



// TODO：构造函数
// 注意：构造函数的作用就是用来创建对象的
// 其用法与Java中构造函数一样，区别在于JS中没有具体的类class
// 而是直接使用构造函数创建，即可
// 构造函数就是一个普通的函数，创建方式和普通函数没有区别
// 区别：
//      a. 构造函数习惯上首字母大写
//      b. 调用方式不同：
//              普通函数调用：方法名();
//              构造函数调用：new 方法名();
// 理解：使用new调用的函数，就被成为“构造函数”
// 注意：构造函数内，不用写return，写了也无效，默认返回新创建的对象
// 构造函数调用时，返回值是object
// 如：
function Person() { }
let person = new Person();          // new：就是创建对象
// 注意：构造函数内的this：指向当前创建的对象
// 例题：
function Person(name, age) {
    // 当使用new Person时，Person就会被当作为一个对象使用
    // 最终的返回值Person也是对象类型（Object的后代）
    // this：指向的当前创建的Person对象
    // this.name：给当前创建的对象挂载属性name
    // this.sayHi：给当前对象挂载方法
    this.name = name;
    this.age = age;
    this.sayHi = function () {
        console.log("你好");
    }
}
let person02 = new Person("张三", 18);
let person03 = new Person("李四", 20);


// TODO：instanceof
// 使用instanceof可以检查一个对象是否是一个类的实例
// 语法：实例对象 instanceof 构造函数名
// 返回值：boolean
// 如：
let bool = person02 instanceof Person;
// 注意：所有的对象都是Object对象的后代
// 所以任何对象和Object做instanceof检查时都会返回true
// 如：
bool = person02 instanceof Object;


// TODO：回调函数
// 由我们创建，但不由我们调用的函数，被称为“回调函数”
// 如：
array.forEach((item) => {
    console.log("我是一个回调函数");
});


// TODO：函数的特殊调用call()和apply()
// 这两个方法都是“函数对象”上的方法
// 我们可以通过函数对象调用这两个方法，从而调用函数
// 如：
function sayHi() { }
// 三者都可以调用函数
sayHi();        // 函数调用
sayHi.apply();  // 函数调用
sayHi.call();   // 函数调用

// call()与apply()的作用：
// 在调用call()和apply()时可以将一个对象指定为“第一个参数”
// 此时这个对象将会成为函数执行时的this
// 即：call()和apply()可以修改函数内this的指向
// 如：
let obj = {};
function sayHi() {
    alert(this);
}
sayHi();        // 此时的函数this指向的是window
sayHi.apply(obj);   // 此时函数内的this指向的是obj
sayHi.call(obj);    // 与apply()一样

// TODO：call()与apply()的区别
// 区别：在于函数本身有形参，call()与apply()传入参数的方式不同
// 如：
function sayHi(a, b) { }
sayHi.call(obj, 2, 3);      // 实参，写在对象的后面
sayHi.apply(obj, [2, 3]);     // 实参，必须封装到一个数组内，再传入


// TODO：封装的实参对象arguments
// arguments：一个类数组对象（不是数组）
// 可以通过索引获取数据，也可以获取长度
let bool02 = Array.isArray(arguments);  // 判断是否是数组
// 在调用函数时，传入的“所有（包括多传入的参数）”实参都会被封装到arguments中
// 传入是实参，是依次保存到arguments中的
// 如：arguments[0]：传入的第一个实参
// arguments[1]：传入的第二个实参

// callee：arguments对象内的一个属性
// callee指向当前函数对象



// TODO：闭包
// 概念：一个函数对周围状态的引用捆绑在一起，内层函数中访问到其外层函数的作用域
// 简单理解：闭包 = 内层函数 + 外层函数的变量
// 如：
function sum05() {
    let a = 10;
    function sum06() {
        // 内层函数使用了外层函数的变量
        // 使得内层函数与外层函数捆绑在一起了
        // 形成了闭包
        console.log(a);
    }
    sum06();
}
// 闭包产生条件：
// 函数内嵌套了一个函数
// 内层函数使用了外层函数的变量（两个函数绑定了）



// TODO：箭头函数
// 语法：() => {}
// 箭头函数直接返回对象
// () => ({属性名:属性值})
// 箭头函数不存在函数提升
// 箭头函数没有arguments多态参数，但是有...arr剩余参数
// 注意：箭头函数没有自己的this。即：箭头函数没有this
// 如果在箭头函数内使用了this，则会在作用域链上寻找this，并使用
// 如：当前箭头函数使用了this，则会去上一层作用域内寻找this



// TODO：实例成员与静态成员
function Person(name, age) {
    // this.name，this.age就是实例成员
    // 实例成员是每个创建的对象都会拥有的属性或方法
    this.name = name;
    this.age = age;
}
// 静态成员
Person.a = 10;      // 像这种定义在构造函数上的成员就是静态成员